<?php
/**
 * Реализация протокола HTTP, т.е. прикладной уровень модели OSI
 * @author Alexander NeonXP Kiryukhin
 * @copyright 2011 Alexander NeonXP Kiryukhin
 * @license GPLv2
 */

class http {

    private
    $sBuffer = '',                                              //@var string буффер хранения данных от клиента
    $arHeader = array('Content-type' => array('text/html')),    //@var array массив заголовков ответа
    $socket,                                                    //@var resource указатель на сокет
    $sStatus = "200 Ok",                                        //@var string статус ответа
    $sType = "GET",                                             //@var string тип запроса
    $sRequestString = "/",                                      //@var string строка запроса
    $sPath = "/",                                               //@var string запрашиваемый адрес
    $sClientProtocol = "HTTP/1.1",                              //@var string версия протокола клиента
    $sClientIp = '',                                            //@var string IP адрес клиента
    $sProtocol = "HTTP/1.1",                                    //@var string версия протокола сервера
    $arClientHeaders = array(),                                 //@var array заголовки запроса от клиента
    $arPost = array(),                                          //@var array массив POST
    $arGet = array(),                                           //@var array массив GET
    $arCookies = array(),                                       //@var array массив COOKIES
    $bConnected = true,                                         //@var boolean флаг наличия соединения
    $sAllowOptions = 'GET,HEAD,POST,OPTIONS';                   //@var string допустимые операции сервера

    static function getInstance($sIncomming, &$socket) {
        return new self($sIncomming, &$socket);
    }
    /**
     * Конструктор
     * Парсит запрос клиента
     * @param string $sIncomming данные от клиента
     * @param reference $socket указатель на сокет
     * @return ничего
     */
    function __construct($sIncomming, &$socket) {
        $this->sBuffer = $sIncomming;
        $this->socket = $socket;
        $arRequest = explode("\n", $sIncomming);
        $sRequest = array_shift($arRequest);
        $bData = false;
        $arData = array();
        $sRequest = trim($sRequest);
        list($sType, $sRequestString, $sProtocol) = explode(' ', $sRequest, 3);
        $this->sClientProtocol = $sProtocol;
        $this->sRequestString = $sRequestString;
        $this->sType = strtoupper(trim($sType));
        socket_getpeername($socket, $this->sClientIp);
        foreach ($arRequest as $sHeader) {
            if (trim($sHeader) != '') {
                if (!$bData) {
                    list($sKey, $sValue) = explode(':', $sHeader, 2);
                    $sKey = trim($sKey);
                    $sValue = trim($sValue);
                    if (isset($this->arClientHeaders[$sKey])) {
                        if (!is_array($this->arClientHeaders[$sKey])) {
                            $this->arClientHeaders[$sKey][0] = $this->arClientHeaders[$sKey];
                        }
                        $this->arClientHeaders[$sKey][] = $sValue;
                    } else {
                        $this->arClientHeaders[$sKey] = $sValue;
                    }
                } else {
                    $arData[] = $sHeader;
                }
            } else {
                $bData = true;
            }
        }
        if (isset($this->arClientHeaders['Cookie'])) {
            $arCookies = explode(";", $this->arClientHeaders['Cookie']);
            foreach ($arCookies as $sCookie) {
                list($sKey, $sValue) = explode('=', $sCookie);
                $this->arCookies[trim($sKey)] = trim($sValue);
            }
        }
        $sData = implode("\n", $arData);
        if (strtoupper(trim($sType)) == 'OPTIONS') {
            $this->addHeader('Allow', $this->sAllowOptions);
        }
        if ((strtoupper(trim($sType)) == 'POST') && (trim($sData) != '')) {
            $arPost = array();
            parse_str($sData, $arPost);
            $this->arPost = $arPost;
        }
        $arUrl = parse_url($sRequestString);
        if (isset($arUrl['path'])) {
            $this->sPath = $arUrl['path'];
        }
        if (isset($arUrl['query'])) {
            $arGet = array();
            parse_str($arUrl['query'], $arGet);
            $this->arGet = $arGet;
        }
    }
    /**
     * headers
     * возращает указанный заголовок или массив всех заголовков
     * @param string $sHeader опционально, название заголовка клиента
     * @return mixed значение заголовка или массив всех заголовков клиента
     */
    function headers($sHeader = null) {
        if ($sHeader) {
            return isset($this->arClientHeaders[$sHeader]) ? $this->arClientHeaders[$sHeader] : null;
        } else {
            return $this->arClientHeaders;
        }
    }
    /**
     * get
     * возращает GET переменную или весь GET массив
     * @param string $sVar опционально, название GET переменной
     * @return mixed значение переменной или массив всех GET переменных
     */
    function get($sVar = null) {
        if ($sVar) {
            return isset($this->arGet[$sVar]) ? $this->arGet[$sVar] : null;
        } else {
            return $this->arGet;
        }
    }
    /**
     * post
     * возращает POST переменную или весь POST массив
     * @param string $sVar опционально, название POST переменной
     * @return mixed значение переменной или массив всех POST переменных
     */
    function post($sVar = null) {
        if ($sVar) {
            return isset($this->arPost[$sVar]) ? $this->arPost[$sVar] : null;
        } else {
            return $this->arPost;
        }
    }
    /**
     * cookies
     * возращает COOKIES переменную или весь COOKIES массив
     * @param string $sVar опционально, название COOKIES переменной
     * @return mixed значение переменной или массив всех COOKIES переменных
     */
    function cookies($sVar = null) {
        if ($sVar) {
            return isset($this->arCookies[$sVar]) ? $this->arCookies[$sVar] : null;
        } else {
            return $this->arCookies;
        }
    }
    /**
     * Возращает тип запроса (GET, POST, OPTIONS или HEAD)
     * @return string Тип запроса
     */
    function requestType() {
        return $this->sType;
    }
    /**
     * Возращает запрашиваемый адрес
     * @return string Адрес
     */
    function requestPath() {
        return $this->sPath;
    }
    /**
     * Возращает версию протокола клиента
     * @return string Версия протокола (например, HTTP/1.1)
     */
    function requestProtocol() {
        return $this->sClientProtocol;
    }
    /**
     * Возращает всю строку запроса
     * @return string Строка запроса (например, GET /hello.html HTTP/1.1)
     */
    function requestString() {
        return $this->sRequestString;
    }
    /**
     * Отправляет клиенту заголовки и начинает вывод данных клиенту
     * @param string $sOut Данные, следующие после заголовков
     * @return http возвращает себя для chain-вызова методов
     */
    function startHTTP($sOut) {
        $sHeaders = "";
        if ($this->arHeader != array()) {
            foreach ($this->arHeader as $sKey => $arValue) {
                foreach ($arValue as $sValue) {
                    $sHeaders .= "\n$sKey: $sValue";
                }
            }
        }
        if (($this->sType != 'HEAD') && ($this->sType != 'OPTIONS')) {
            $sMsg = $this->sProtocol . " " . $this->sStatus . "$sHeaders\n\n$sOut";
        } else {
            $sMsg = $this->sProtocol . " " . $this->sStatus . "$sHeaders";
        }
        socket_write($this->socket, $sMsg, strlen($sMsg));
        return $this;
    }
    /**
     * Отправляет очередные данные клиенту
     * @param string $sMsg Данные для отправки
     * @return http возвращает себя для chain-вызова методов
     */
    function writeHTTP($sMsg) {
        if (($this->sType != 'HEAD') && ($this->sType != 'OPTIONS') && ($this->bConnected)) {
            socket_write($this->socket, $sMsg, strlen($sMsg));
        }
        return $this;
    }
    /**
     * Закрывает соединение с клиентом
     * @return http возвращает себя для chain-вызова методов
     */
    function closeHTTP() {
        if ($this->bConnected) {
            socket_close($this->socket);
            $this->bConnected = false;
        }
        return $this;
    }
    /**
     * Проверка соединения
     * @return boolean статус соединения
     */
    function isConnected() {
        return $this->bConnected;
    }

    /**
     * Устанавливает статус ответа
     * @param string $sStatus статус ответа, например, "200 Ok"
     * @return http возвращает себя для chain-вызова методов
     */
    function setResponseStatus($sStatus) {
        $this->sStatus = $sStatus;
        return $this;
    }
    /**
     * Явное указание версии протокола ответа
     * @param string $sProtocol версия протокола, например, "HTTP/1.1"
     * @return http возвращает себя для chain-вызова методов
     */
    function setProtocol($sProtocol) {
        $this->sProtocol = $sProtocol;
    }
    /**
     * Устанавливает клиенту cookies. Работает аналогично встроенной в PHP функции.
     * Внимание! Вызывать этот метод следует ДО startHTTP()
     * @param type $sName имя куки
     * @param type $sValue значение куки
     * @param type $iExpires дата истечения действия куки в формате UNIX TIMESTAMP
     * @param type $sPath адрес действия куки
     * @param type $sDomain домен действия куки
     * @param type $bSecure использовать только для https соединения?
     * @return http возвращает себя для chain-вызова методов
     */
    function setCookie($sName, $sValue, $iExpires = null, $sPath = null, $sDomain = null, $bSecure = false) {
        $sCookie = "$sName = $sValue" .
                (($iExpires) ? '; expires=' . date('r', $iExpires) : '') .
                (($sPath) ? '; path=' . $sPath : '') .
                (($sDomain) ? '; domain=' . $sDomain : '') .
                (($bSecure) ? '; secure' : '');
        $this->addHeader('Set-Cookie', $sCookie);
        $this->arCookies[$sName] = $sValue;
        return $this;
    }
    /**
     * Добавляет заголовки в ответ
     * @param string $sName Название заголовка
     * @param string $sValue Значение заголовка
     * @param string $bClean Перезаписать одноименный заголовок или дописать
     * @return http возвращает себя для chain-вызова методов
     */
    function addHeader($sName, $sValue, $bClean=false) {
        if ($bClean) {
            $this->arHeader[$sName] = array($sValue);

            } else {
            $this->arHeader[$sName][] = $sValue;
            }
        return $this;

    }

}

